/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.client.rpc;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Element;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Map.Entry;

/**
 * For internal use only. Used for server call serialization.
 * @skip 
 */
public class ClientSerializationStream extends SerializationStream {
  Serializer serializer;
  Element fContainer;
  StringBuffer fEncodingBuffer;
  String nextToken;
  Element fChild;

  ArrayList fSeenArray = new ArrayList();
  int fSeenCount;

  /*
   * Map of name to index
   */
  HashMap fSeenStrings = new HashMap();

  // For use by native methods in hosted mode.
  //
  // Element fSeenMap, fSeenArray;
  HashMap fSeenMap = new HashMap();

  // Array of results from an RPC call.
  JavaScriptObject fResultsArray;

  // Array of typenames used in an RPC call
  JavaScriptObject fStringTable;

  public ClientSerializationStream(Serializer serializer) {
    this.serializer = serializer;
  }

  private static void append(StringBuffer sb, String token) {
    // The line below expresses that a null token should never make it here.
    // Since we treat java.lang.String just like any other object a null
    // string should be encoded as a '~' character in the stream.  Unfortunately,
    // since this is translatable code I cannot use an assert in this code.
    //
    // assert(token != null);
    //
    sb.append(token);
    sb.append('\uffff');
  }

  /**
   * Appends a token to the end of the buffer.
   */
  public void append(String token) {
    append(fEncodingBuffer, token);
  }

  /**
   * Called during encode to encode a backref if the object has been seen
   * before.
   * 
   * @param o the object reference to be stored.
   * @return <code>true</code> if the caller needs to encode the object (If o
   *         is non-null, never returns true twice).
   */
  public boolean startInstance(SerializationStreamWriter streamWriter,
      Object instance) {
    if (instance != null) {
      int index = addString(getObjectTypeName(instance));

      int prevIdx = privLookupAndRememberAlreadyEncodedObject(instance, System
        .identityHashCode(instance));
      if (prevIdx != -1) {
        append(PREV_INSTANCE_MARKER + prevIdx);
        return false;
      }

      append("+" + index);
      return true;
    }

    append("~");
    return false;
  }

  public void endInstance(SerializationStreamWriter streamWriter,
      Object instance) {
    // Purposely empty
  }

  private String getObjectTypeName(Object o) {
    String typeName = GWT.getTypeName(o); 
    String serializationSignature = serializer.getSerializationSignature(typeName);
    if (serializationSignature == null) {
      return typeName;
    }
    
    return typeName + "/" + serializationSignature;
  }

  /**
   * Returns the next token in the decoded stream. Do not call this method
   * unless you know it will succeed, either because you have foreknowledge of
   * the number of tokens (which is most efficient) or a call to
   * {@link ClientTokenBuffer#canExtract} has just succeeded.
   * 
   * @return The next token in the stream as a String, or null if a token value
   *         is absent.
   */
  public native String extract() /*-{ 
    var resVal = this.@com.google.gwt.user.client.rpc.ClientSerializationStream::fResultsArray.pop();
    return resVal;
  }-*/;

  /**
   * Finds the already-decoded object associated with the given id. Note that in
   * this case, __seen is treated as an array.
   * 
   * @param indexToken has leading '#'
   */
  public Object lookupDecodedObject(String indexToken) {
    String indexString = indexToken.substring(1);
    int prev = Integer.parseInt(indexString);
    return fSeenArray.get(prev);
  }

  protected native JavaScriptObject evalArray(String encoded) /*-{
    var array = eval(encoded);
    return array; 
  }-*/;
  
  /**
   * Call this method before attempting to extract any tokens. This method
   * implementation <b>must</b> be called by any overridden version.
   */
  public void prepareToRead(String encoded) {
    clearSeenObjects();
    fResultsArray = evalArray(encoded);
    
    // Read the stream version number
    //
    setVersion(Integer.parseInt(extract()));
    
    // Read the flags from the stream
    //
    setFlags(Integer.parseInt(extract()));

    fStringTable = getStringTableFromStream();
  }

  private native JavaScriptObject getStringTableFromStream() /*-{
    return this.@com.google.gwt.user.client.rpc.ClientSerializationStream::fResultsArray.pop(); 
  }-*/;

  /**
   * Call this method before attempting to append any tokens. This method
   * implementation <b>must</b> be called by any overridden version.
   */
  public void prepareToWrite() {
    clearSeenObjects();
    fEncodingBuffer = new StringBuffer();
  }

  public Object prevInstance(SerializationStreamReader stream) {
    nextToken = extract();
    if (nextToken.startsWith(PREV_INSTANCE_MARKER)) {
      return lookupDecodedObject(nextToken);
    }

    return null;
  }

  public String nextInstance(SerializationStreamReader stream) {
    String current = nextToken;
    nextToken = null;
    if (current.equals("~")) {
      return null;
    }

    return getInstanceName(current);
  }

  /**
   * Called during decode to allow an object to be coalesced Note that in this
   * case, __seen is treated as an array.
   */
  public void rememberDecodedObject(Object o) {
    // Add version
    fSeenArray.add(o);
  }

  public String toString() {
    StringBuffer buffer = new StringBuffer();
    
    writeHeader(buffer);
    
    writeStringTable(buffer);
    
    writePayload(buffer);
    
    String packet = buffer.toString();
    
    return packet;
  }

  private void writePayload(StringBuffer packetBuffer) {
    packetBuffer.append(fEncodingBuffer.toString());
  }

  /**
   * @param packetBuffer
   */
  private void writeHeader(StringBuffer packetBuffer) {
    append(packetBuffer, Integer.toString(SerializationStream.SERIALIZATION_STREAM_VERSION));
    append(packetBuffer, Integer.toString(getFlags()));
  }

  private StringBuffer writeStringTable(StringBuffer sb) {
    int stringTableSize = fSeenStrings.size();
    String[] stringTable = new String[stringTableSize];
    Set entrySet = fSeenStrings.entrySet();
    Iterator iter = entrySet.iterator();
    while (iter.hasNext()) {
      Entry entry = (Entry) iter.next();

      stringTable[((Integer) entry.getValue()).intValue()] = (String) entry
        .getKey();
    }

    append(sb, Integer.toString(stringTableSize));

    for (int index = 0; index < stringTableSize; ++index) {
      append(sb, stringTable[index]);
    }

    return sb;
  }

  /**
   * Clears the list of already-seen objects. Note that __seen is used
   * differently in the encode and decode cases. __seenCnt is only used for
   * encode.
   */
  protected void clearSeenObjects() {
    this.fSeenArray.clear();
    this.fSeenMap.clear();
    this.fSeenCount = 0;
  }

  protected void clearStringTable() {
    fSeenStrings.clear();
    fStringTable = null;
  }

  public int addString(String typeName) {
    Object o = fSeenStrings.get(typeName);
    int index;
    if (o != null) {
      index = ((Integer) o).intValue();
    } else {
      index = fSeenStrings.size();
      fSeenStrings.put(typeName, new Integer(index));
    }

    return index;
  }

  public native int getStringCount() /*-{
    return this.@com.google.gwt.user.client.rpc.ClientSerializationStream::fStringTable.length;
  }-*/;
  
  public native String getString(int token) /*-{ 
    return this.@com.google.gwt.user.client.rpc.ClientSerializationStream::fStringTable[token]; 
  }-*/;

  protected void finalize() throws Throwable {
    // Not called in web mode. This exists solely to ensure that objects
    // allocated
    // in native code get cleaned up when this class is gc'd.
    //
    clearSeenObjects();
  }

  /**
   * Native helper for {@link #encodeObjectPrelude}. If the object has been
   * seen before, returns its index. If not, returns -1 and remembers the object
   * in order. Note that in this case, __seen is treated as a map.
   * 
   * @param o the object to be remembered.
   * @param hashCode the result of calling {@link System#identityHashCode} on o.
   */
  protected int privLookupAndRememberAlreadyEncodedObject(Object o, int hashCode) {
    Integer x = (Integer) this.fSeenMap.get(new Integer(hashCode));
    if (x != null)
      return x.intValue();
    this.fSeenMap.put(new Integer(hashCode), new Integer(this.fSeenCount++));
    return -1;
  }

  public SerializationStreamObjectDecoder getObjectDecoder() {
    return this;
  }

  public SerializationStreamObjectEncoder getObjectEncoder() {
    return this;
  }

  protected String getInstanceName(String token) {
    int nameIndex = Integer.parseInt(token.substring(1));
    return getString(nameIndex);
  }

  public Object readObject() throws SerializationException {
    Object obj = serializer.deserialize(this);
    return obj;
  }

  public void writeObject(Object instance) throws SerializationException {
    serializer.serialize(this, instance);
  }

  public StringTable getStringTable() {
    return this;
  }
}
